Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.96% covered (success)
95.96%
95 / 99
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Serializer
95.96% covered (success)
95.96%
95 / 99
66.67% covered (warning)
66.67%
4 / 6
30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 normalize
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
14.06
 denormalizeOnMethodCall
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 denormalizeNewObject
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
11.04
 denormalizeOnExistingObject
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\Serializer;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Exceptions\InvalidTypeException;
6use Apie\Core\Lists\ItemHashmap;
7use Apie\Core\Lists\ItemList;
8use Apie\Core\Metadata\Concerns\UseContextKey;
9use Apie\Core\Metadata\MetadataFactory;
10use Apie\Core\Utils\ConverterUtils;
11use Apie\Core\ValueObjects\Utils;
12use Apie\Serializer\Context\ApieSerializerContext;
13use Apie\Serializer\Context\NormalizeChildGroup;
14use Apie\Serializer\Exceptions\ValidationException;
15use Apie\Serializer\FieldFilters\FieldFilterInterface;
16use Apie\Serializer\FieldFilters\NoFiltering;
17use Apie\Serializer\Interfaces\DenormalizerInterface;
18use Apie\Serializer\Interfaces\NormalizerInterface;
19use Apie\Serializer\Lists\NormalizerList;
20use Apie\Serializer\Normalizers\AliasDenormalizer;
21use Apie\Serializer\Normalizers\BooleanNormalizer;
22use Apie\Serializer\Normalizers\DateTimeNormalizer;
23use Apie\Serializer\Normalizers\DateTimeZoneNormalizer;
24use Apie\Serializer\Normalizers\DoNotChangeFileNormalizer;
25use Apie\Serializer\Normalizers\EnumNormalizer;
26use Apie\Serializer\Normalizers\FloatNormalizer;
27use Apie\Serializer\Normalizers\IdentifierNormalizer;
28use Apie\Serializer\Normalizers\IntegerNormalizer;
29use Apie\Serializer\Normalizers\ItemListNormalizer;
30use Apie\Serializer\Normalizers\PaginatedResultNormalizer;
31use Apie\Serializer\Normalizers\PermissionListNormalizer;
32use Apie\Serializer\Normalizers\PolymorphicObjectNormalizer;
33use Apie\Serializer\Normalizers\ReflectionTypeNormalizer;
34use Apie\Serializer\Normalizers\ResourceNormalizer;
35use Apie\Serializer\Normalizers\StringableCompositeValueObjectNormalizer;
36use Apie\Serializer\Normalizers\StringNormalizer;
37use Apie\Serializer\Normalizers\UploadedFileNormalizer;
38use Apie\Serializer\Normalizers\ValueObjectNormalizer;
39use Exception;
40use Psr\Http\Message\UploadedFileInterface;
41use ReflectionClass;
42use ReflectionMethod;
43
44class Serializer
45{
46    use UseContextKey;
47
48    public function __construct(private NormalizerList $normalizers)
49    {
50    }
51
52    /**
53     * @param iterable<int, NormalizerInterface|DenormalizerInterface> $additionalNormalizers
54     */
55    public static function create(iterable $additionalNormalizers = []): self
56    {
57        return new self(new NormalizerList([
58            ...$additionalNormalizers,
59            new AliasDenormalizer(),
60            new PaginatedResultNormalizer(),
61            new DoNotChangeFileNormalizer(),
62            new PermissionListNormalizer(),
63            new UploadedFileNormalizer(),
64            new IdentifierNormalizer(),
65            new StringableCompositeValueObjectNormalizer(),
66            new PolymorphicObjectNormalizer(),
67            new DateTimeNormalizer(),
68            new DateTimeZoneNormalizer(),
69            new ResourceNormalizer(),
70            new EnumNormalizer(),
71            new ValueObjectNormalizer(),
72            new StringNormalizer(),
73            new IntegerNormalizer(),
74            new FloatNormalizer(),
75            new BooleanNormalizer(),
76            new ItemListNormalizer(),
77            new ReflectionTypeNormalizer(),
78        ]));
79    }
80
81    public function normalize(mixed $object, ApieContext $apieContext, bool $forceDefaultNormalization = false): string|int|float|bool|ItemList|ItemHashmap|null
82    {
83        $serializerContext = new ApieSerializerContext($this, $apieContext);
84        if (!$forceDefaultNormalization) {
85            foreach ($this->normalizers->iterateOverNormalizers() as $normalizer) {
86                if ($normalizer->supportsNormalization($object, $serializerContext)) {
87                    return $normalizer->normalize($object, $serializerContext);
88                }
89            }
90        }
91
92        $fieldFilter = $apieContext->getContext(FieldFilterInterface::class, false) ? : new NoFiltering();
93
94        if (is_array($object)) {
95            $count = 0;
96            $returnValue = [];
97            $isList = true;
98            // TODO: should a field filter have effect on arrays?
99            foreach ($object as $key => $value) {
100                if ($key === $count) {
101                    $count++;
102                } else {
103                    $isList = false;
104                }
105                $returnValue[$key] = $serializerContext->normalizeChildElement($key, $value);
106            }
107            return $isList ? new ItemList($returnValue) : new ItemHashmap($returnValue);
108        }
109        if (!is_object($object)) {
110            if (in_array(get_debug_type($object), ['resource', 'resource (closed)'])) {
111                throw new InvalidTypeException($object, 'primitive');
112            }
113            return $object;
114        }
115        $metadata = MetadataFactory::getResultMetadata(new ReflectionClass($object), $apieContext);
116        $returnValue = [];
117
118        foreach ($metadata->getHashmap()->filterOnContext($apieContext, getters: true) as $fieldName => $metadata) {
119            if ($metadata->isField() && $fieldFilter->isFiltered($fieldName)) {
120                $returnValue[$fieldName] = $serializerContext->normalizeChildElement(
121                    $fieldName,
122                    $metadata->getValue($object, $apieContext)
123                );
124            }
125        }
126        return new ItemHashmap($returnValue);
127    }
128
129    public function denormalizeOnMethodCall(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $input, ?object $object, ReflectionMethod $method, ApieContext $apieContext): mixed
130    {
131        $serializerContext = new ApieSerializerContext($this, $apieContext);
132        try {
133            $arguments = $serializerContext->denormalizeFromMethod($input, $method);
134        } catch (Exception $error) {
135            throw ValidationException::createFromArray(['' => $error]);
136        }
137        return $method->invokeArgs($object, $arguments);
138    }
139
140    public function denormalizeNewObject(string|int|float|bool|ItemList|ItemHashmap|array|null|UploadedFileInterface $object, string $desiredType, ApieContext $apieContext): mixed
141    {
142        if (is_array($object)) {
143            $isList = false;
144            if ($desiredType === 'mixed') {
145                $isList = true;
146                $count = 0;
147                foreach (array_keys($object) as $key) {
148                    if ($key === $count) {
149                        $count++;
150                    } else {
151                        $isList = false;
152                        break;
153                    }
154                }
155            }
156            $object = $isList ? new ItemList($object) : new ItemHashmap($object);
157        }
158        if ($desiredType === 'mixed') {
159            return $object;
160        }
161        $serializerContext = new ApieSerializerContext($this, $apieContext);
162        foreach ($this->normalizers->iterateOverDenormalizers() as $denormalizer) {
163            if ($denormalizer->supportsDenormalization($object, $desiredType, $serializerContext)) {
164                return $denormalizer->denormalize($object, $desiredType, $serializerContext);
165            }
166        }
167        $refl = ConverterUtils::toReflectionClass($desiredType);
168        if (!$refl || !$refl->isInstantiable()) {
169            throw new InvalidTypeException($desiredType, 'a instantiable object');
170        }
171        $metadata = MetadataFactory::getCreationMetadata(
172            $refl,
173            $apieContext
174        );
175        $group = new NormalizeChildGroup(
176            $serializerContext,
177            $metadata
178        );
179        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
180        return $normalizedData->createNewObject();
181    }
182
183    public function denormalizeOnExistingObject(ItemHashmap $object, object $existingObject, ApieContext $apieContext): mixed
184    {
185        $refl = new ReflectionClass($existingObject);
186        $serializerContext = new ApieSerializerContext($this, $apieContext);
187        $metadata = MetadataFactory::getModificationMetadata(
188            $refl,
189            $apieContext
190        );
191        $group = new NormalizeChildGroup(
192            $serializerContext,
193            $metadata
194        );
195        $normalizedData = $group->buildNormalizedData($refl, Utils::toArray($object));
196        return $normalizedData->modifyExistingObject($existingObject);
197    }
198}